/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package shaman.storage.impl.file;

import com.thoughtworks.xstream.XStream;
import shaman.storage.api.Filter;
import shaman.storage.impl.file.date.UnixDateFormat;
import java.awt.Frame;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import shaman.storage.api.Backup;
import shaman.storage.common.FileProperties;
import shaman.storage.impl.CompositeFilter;
import shaman.storage.impl.stream.gather.PartStreamFactory;
import shaman.storage.impl.util.ObjectStreamIterable;

/**
 *
 * @author dmitriy
 */
public class FileBackup implements Backup {

    public static final String COMPONENT_FILES="files";
    public static final String COMPONENT_BLOCKS="blocks";
    public static final String COMPONENT_DATA="data";

    public static final String METADATA_EXT="bmd";
    public static final String DATA_EXT="dat";

    public static final String DIGEST_ALGORITHM="MD5";
    
    public static final int FILE_BLOCK_SIZE=32768;

    private final Map<String, FilePartAccessor> streams=new HashMap<String, FilePartAccessor>();
    private final StorageFileAccessor acc;
    private final Date backupDate;
    private final String dateString;
    private final CompositeFilter preprocessor, mainFilter, postprocessor;
    private final long partSizeLimit, blockSizeLimit;
    private final FilePropertiesCache cache;
    private final BlockAccessor bacc;
    private final BackupFinishListener finishListener;

    private boolean closed;

    public FileBackup(StorageFileAccessor acc, FileBackupMetadata md, Date backupDate, BackupFinishListener finishListener) throws IOException {
        this.acc = acc;
        this.backupDate=backupDate;
        dateString=getDateString(backupDate);
        preprocessor=new CompositeFilter(md.preprocessors);
        mainFilter=new CompositeFilter(md.mainFilters);
        postprocessor=new CompositeFilter(md.postprocessors);
        partSizeLimit=md.partSizeLimit;
        blockSizeLimit=md.blockSizeLimit;
        if (md==null)
        {
            md=readMetadata();
            closed=true;
        }
        else {
            addPartStreamFactory(COMPONENT_FILES);
            addPartStreamFactory(COMPONENT_DATA);
            closed=false;
        }
        ArrayList mdFilters=new ArrayList(preprocessor.getFilters());
        mdFilters.addAll(mainFilter.getFilters());
        FileContentsAccessor filesAccessor=new FileMetadataAccessor(getPartStreamFactory(COMPONENT_FILES), mdFilters);
        if (closed)
        {
            cache=new FilePropertiesCache(filesAccessor);
            bacc=new BlockAccessor(streams.get(COMPONENT_DATA), mainFilter, postprocessor);
        }
        else {
            cache=new FilePropertiesCache(filesAccessor, closed);
            bacc=new BlockAccessor(getPartStreamFactory(COMPONENT_DATA), mainFilter, postprocessor, blockSizeLimit, cache);
        }
        this.finishListener=finishListener;
    }

    protected static String getDateString(Date date) {
        StringBuffer buf=new StringBuffer();
        UnixDateFormat.format(date, buf);
        return buf.toString();
    }

    public Date getBackupDate() throws IOException {
        return backupDate;
    }

    public Iterable<String> getFileList() throws IOException {
        return new FileList(cache);
    }

    public FileProperties getFileProperties(String fileName) throws IOException, FileNotFoundException {
        return cache.fetch(fileName);
    }

    public InputStream getFileData(String fileName) throws IOException, FileNotFoundException {
        StoredFileProperties file=cache.fetch(fileName);
        if (file==null)
            throw new RuntimeException("Error getting file properties");
        return bacc.fetchFile(file);
    }

    public OutputStream createFile(String fileName, Map<String, String> properties) throws IOException {
        StoredFileProperties file=cache.createFile(fileName, properties);
        return bacc.storeFile(file);
    }

    public void finish() throws IOException {
        bacc.setImmutable();
        cache.stopWriting();
        writeMetadata();
        closed=true;
        if (finishListener!=null)
            finishListener.finishBackup(backupDate);
    }

    public boolean hasConfigForm() {
        return false;
    }

    public boolean showConfigForm(Frame parent) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public List<String> getPropertyNames() {
        return Collections.emptyList();
    }

    public String getPropertyValue(String name) throws InvalidParameterException {
        throw new InvalidParameterException("No properties supported");
    }

    public List<String> getPropertyValues(String name) throws InvalidParameterException {
        throw new InvalidParameterException("No properties supported");
    }

    public void setPropertyValue(String name, String value) throws InvalidParameterException {
        throw new InvalidParameterException("No properties supported");
    }

    public static String getFileName(String dateString, String component, int ordinal, String ext) {
        StringBuffer buf = new StringBuffer(component);
        buf.append("_").append(dateString);
        if (ordinal > 0) {
            buf.append("_").append(ordinal);
        }
        if (ext != null && ext.length() > 0) {
            buf.append(".").append(ext);
        }
        return buf.toString();
    }

    private FileBackupMetadata readMetadata() throws IOException, FileNotFoundException {
        FileBackupMetadata md;
        InputStream in=acc.fetchFile(FileBackup.getFileName(dateString, FileBackup.COMPONENT_BLOCKS, 0, FileBackup.METADATA_EXT), 0, -1);
        ObjectInputStream oin=null;
        try{
            XStream xs=new XStream();
            xs.processAnnotations(new Class[]{
                FileBackupMetadata.class,
                StoredBlock.class
            });
            oin=xs.createObjectInputStream(in);
            Object obj;
            try {
                obj=oin.readObject();
            } catch (ClassNotFoundException ex) {
                obj=null;
            }
            if (!(obj instanceof FileBackupMetadata))
                throw new IOException("No backup metadata found");
            md=(FileBackupMetadata) obj;
            if (md.getVersion()!=FileBackupMetadata.VERSION_1)
                throw new IOException("Unknown metadata version");
            for (StoredBlock block: new ObjectStreamIterable<StoredBlock>(oin, StoredBlock.class)) {
                String fileType=block.getStream();
                FilePartAccessor pacc=streams.get(fileType);
                if (pacc==null)
                {
                    pacc=new FilePartAccessor(acc, fileType, FileBackup.DATA_EXT, postprocessor, dateString, partSizeLimit);
                    streams.put(fileType, pacc);
                }
                pacc.addBlock(block);
            }
        } finally {
            if (oin!=null) oin.close();
            in.close();
        }
        return md;
    }

    private void writeMetadata() throws IOException, FileNotFoundException {
        OutputStream out=acc.storeFile(FileBackup.getFileName(dateString, FileBackup.COMPONENT_BLOCKS, 0, FileBackup.METADATA_EXT));
        ObjectOutputStream oout=null;
        FileBackupMetadata md=new FileBackupMetadata();
        md.preprocessors=preprocessor.getFilters();
        md.mainFilters=mainFilter.getFilters();
        md.postprocessors=postprocessor.getFilters();
        md.partSizeLimit=partSizeLimit;
        md.blockSizeLimit=blockSizeLimit;
        try{
            XStream xs=new XStream();
            xs.processAnnotations(new Class[]{
                FileBackupMetadata.class,
                StoredBlock.class
            });
            oout=xs.createObjectOutputStream(out, FileBackup.COMPONENT_BLOCKS);
            oout.writeObject(md);
            for (FilePartAccessor pacc: streams.values())
            {
                for (StoredBlock obj: pacc) {
                    oout.writeObject(obj);
                }
            }
        } finally {
            if (oout!=null) oout.close();
            out.close();
        }
    }

    private PartStreamFactory getPartStreamFactory(String component) {
        return streams.get(component);
    }

    private PartStreamFactory addPartStreamFactory(String component) {
        FilePartAccessor fact=streams.get(component);
        if (fact==null)
        {
            fact=new FilePartAccessor(acc, component, DATA_EXT, postprocessor, dateString, partSizeLimit);
            streams.put(component, fact);
        }
        return fact;
    }

    public List<Filter> getFilters() {
        if (!closed)
            throw new IllegalStateException("Cannot get filters from a writable backup");
        List<Filter> ret=new ArrayList<Filter>();
        ret.addAll(preprocessor.getFilters());
        ret.addAll(mainFilter.getFilters());
        ret.addAll(postprocessor.getFilters());
        return ret;
    }

}
